/**
 * @Created Nov 4, 2011 9:41:05 AM
 * @author cry30
 */
package com.philip.journal.core.dao;

/**
 | File:    HibernateAuditLogListener.java
 | Created: Mar 3, 2008
 | Author:  Will Hoover
 */
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.StatelessSession;
import org.hibernate.cfg.Configuration;
import org.hibernate.event.AbstractPreDatabaseOperationEvent;
import org.hibernate.event.Initializable;
import org.hibernate.event.PreDeleteEvent;
import org.hibernate.event.PreDeleteEventListener;
import org.hibernate.event.PreInsertEvent;
import org.hibernate.event.PreInsertEventListener;
import org.hibernate.event.PreUpdateEvent;
import org.hibernate.event.PreUpdateEventListener;

import com.philip.core.WarningType;
import com.philip.dao.AuditTrail;
import com.philip.journal.core.bean.User;

/**
 * Audit Log Listener is used to log insert, update, delete, and load operations. Complete list of listeners/events can
 * be obtained at <tt>org.hibernate.event.EventListeners</tt>.
 *
 * @see org.hibernate.event.EventListeners
 * @author whoover
 */
public final class HibernateAuditLogListener implements PreDeleteEventListener, PreInsertEventListener,
        PreUpdateEventListener, Initializable {

    /** Class logger. */
    private static final Log LOGGER = LogFactory.getLog(HibernateAuditLogListener.class);

    /** Insert Operation Type. */
    public static final String OP_TYPE_INSERT = "INSERT";

    /** Update Operation Type. */
    public static final String OP_TYPE_UPDATE = "UPDATE";

    @Override
    public void initialize(final Configuration cfg)
    {
        //
    }

    /** We don't include this in the audit trail. */
    private final List<String> auditColumn = Arrays.asList(new String[] {
            "creator",
            "updater",
            "createDate",
            "createTime",
            "updateDate",
            "updateTime" });

    /**
     * Simply gets the updater in the Entity. Until a session aware method is discovered, we will rely on the client
     * side actor set on the entity to determine the actor. Another option is to pass the session data lower down to the
     * DAO side.
     *
     * @param session Hibernate session.
     * @param event pre action event.
     * @return the current updater set on the Entity.
     */
    private String getActorId(final StatelessSession session, final AbstractPreDatabaseOperationEvent event)
    {
        final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
        User actor = ((User) event.getPersister().getPropertyValue(event.getEntity(), "updater", entityMode));
        if (actor == null) {//can happen due to bad data.  I do not set the creator and updater before so...
            actor = ((User) event.getPersister().getPropertyValue(event.getEntity(), "creator", entityMode));
        }
        return actor.getUsername();
    }

    @Override
    public boolean onPreDelete(final PreDeleteEvent event)
    {
        try {
            @SuppressWarnings(WarningType.DEPRECATION)
            final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister()
                    .getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity())) : null;
            final String entityName = event.getEntity().getClass().toString();
            final Date transTime = new Date(); // new Date(event.getSource().getTimestamp());

            // need to have a separate session for audit save
            final StatelessSession session = event.getPersister().getFactory().openStatelessSession();
            session.beginTransaction();
            final String actorId = getActorId(session, event);

            session.insert(new AuditTrail(entityId.toString(), entityName, actorId, transTime));
            session.getTransaction().commit();
        } catch (final HibernateException e) {
            LOGGER.error("Unable to process audit log for DELETE operation", e);
        }
        return false;
    }

    @Override
    public boolean onPreInsert(final PreInsertEvent event)
    {
        try {
            @SuppressWarnings(WarningType.DEPRECATION)
            final String entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister()
                    .getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity()))
                    .toString() : "";
            final String entityName = event.getEntity().getClass().toString();
            final Date transTime = new Date(); // new Date(event.getSource().getTimestamp());
            final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
            Object newPropValue = null;

            // need to have a separate session for audit save
            final StatelessSession session = event.getPersister().getFactory().openStatelessSession();
            session.beginTransaction();

            final String actorId = getActorId(session, event);
            for (final String propertyName : event.getPersister().getPropertyNames()) {
                if (!auditColumn.contains(propertyName)) {
                    newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
                    // because we are performing an insert we only need to be concerned will non-null values
                    if (newPropValue != null && !(newPropValue instanceof Collection)) {
                        final AuditTrail auditTrail = new AuditTrail(entityId, entityName, OP_TYPE_INSERT, actorId,
                                transTime).entityProperty(propertyName).entityPropNewValue(
                                newPropValue == null ? null : newPropValue.toString());
                        session.insert(auditTrail);
                    }
                }
            }

            session.getTransaction().commit();
        } catch (final HibernateException e) {
            LOGGER.error("Unable to process audit log for INSERT operation", e);
        }
        return false;
    }

    @Override
    public boolean onPreUpdate(final PreUpdateEvent event)
    {
        try {
            @SuppressWarnings(WarningType.DEPRECATION)
            final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister()
                    .getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity())) : null;
            final String entityName = event.getEntity().getClass().toString();
            final Date transTime = new Date(); // new Date(event.getSource().getTimestamp());
            final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
            Object oldPropValue = null;
            Object newPropValue = null;

            // need to have a separate session for audit save
            final StatelessSession session = event.getPersister().getFactory().openStatelessSession();
            session.beginTransaction();

            // get the existing entity from session so that we can extract existing property values
            final Object existingEntity = session.get(event.getEntity().getClass(), entityId);
            final String actorId = getActorId(session, event);

            // cycle through property names, extract corresponding property values and insert new entry in audit trail
            for (final String propertyName : event.getPersister().getPropertyNames()) {
                if (!auditColumn.contains(propertyName)) {
                    newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
                    oldPropValue = event.getPersister().getPropertyValue(existingEntity, propertyName, entityMode);

                    // because we are performing an insert we only need to be concerned will non-null values
                    if (newPropValue != null && !newPropValue.equals(oldPropValue)
                            && !(newPropValue instanceof Collection)) {

                        final AuditTrail auditTrail = new AuditTrail(entityId.toString(), entityName, OP_TYPE_UPDATE,
                                actorId, transTime).entityProperty(propertyName)
                                .entityPropOldValue(oldPropValue == null ? null : oldPropValue.toString())
                                .entityPropNewValue(newPropValue == null ? null : newPropValue.toString());
                        session.insert(auditTrail);
                    }
                }
            }

            session.getTransaction().commit();
        } catch (final HibernateException e) {
            LOGGER.error("Unable to process audit log for UPDATE operation", e);
        }
        return false;
    }
}
